import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import javax.imageio.ImageIO;


public class ColorParser implements IParser{
	private VideoFile videoFile;
	private IColorStrategy colorMethod;
	private BufferedImage image = null;
	private FrameColorInfo[] frameInfos;
	private double[] histogramDiffs;
	private double meanHistogramDiff;
	private double deviationHistogramDiff;
	private double cutThreshold;
	private double transitionThreshold;
	
	// shot boundaries
	private ArrayList<Integer> starts = null;
	private ArrayList<Integer> ends = null;
	private ArrayList<Integer> cuts = null;

	
	public ColorParser(VideoFile videoFile, IColorStrategy colorMethod){
		this.videoFile = videoFile;
		this.colorMethod = colorMethod;
		image = new BufferedImage(videoFile.getWidth(), videoFile.getHeight(),
				BufferedImage.TYPE_INT_RGB);
		
		//Initialize informations of all video frames
		frameInfos = new FrameColorInfo[(int) videoFile.getTotalFrameNumber()];
		for(int i=0; i<frameInfos.length; ++i){
			frameInfos[i] = new FrameColorInfo(colorMethod.getBinSize());
		}
		histogramDiffs = new double[frameInfos.length]; //index 0 is NOT used
		
		starts = new ArrayList<Integer>(50);
		ends = new ArrayList<Integer>(50);
		cuts = new ArrayList<Integer>(50);
		
	}
	
	public void genKeyFrames(VideoSegment[] segments){
		
		for(int i=0; i<segments.length; i++){
			
			VideoSegment seg = segments[i];
			if(seg == null) continue;
			int lastKeyFrame = seg.start;
			seg.keyFrames.add(seg.start);
			//saveFrameImage(lastKeyFrame, "key");
			for(int j=seg.start+1; j < seg.end; j++){
				if(calcSingleHistogramDiffSum(
						frameInfos[lastKeyFrame].histogramValue,
						frameInfos[j].histogramValue) > this.cutThreshold * 0.7){
					if(j - lastKeyFrame > 24){
						seg.keyFrames.add(j);
						lastKeyFrame = j;
						//saveFrameImage(lastKeyFrame, "key");
					}
				}
			}
			
		}
	}
	
	public double[] getHistogramDiffs(){
		return histogramDiffs;
	}
	
	public FrameColorInfo[] getVideoInfoArray(){
		return frameInfos;
	}
	
	public void parse(VideoSegment[] segments){
		System.out.println("Color information parsing starts");
		this.calcAllHistogramValues();
		this.calcAllHistogramDiffs();
		this.calcMeanDeviationHistogramDiff();
		System.out.println("Mean: " + meanHistogramDiff + "; Deviation: " + deviationHistogramDiff);
		this.cutThreshold = meanHistogramDiff + 3.0f * deviationHistogramDiff;
		this.transitionThreshold = meanHistogramDiff; //May be fixed later
		System.out.println("Cut threshold: " + cutThreshold);
		this.detectShotBoundaries();
	}
	
	private void calcAllHistogramValues(){
		byte[] rgbData;
		int colorCode;
		int ind;
		int size = videoFile.getWidth() * videoFile.getHeight();
		int doubleSize = 2 * size;
		
		for(int i=0; i<frameInfos.length; ++i){
			try {
				rgbData= videoFile.readRawData();
			} catch (IOException e) {
				e.printStackTrace();
				return;
			}

			//Reset the index of an image
			for (ind=0; ind<size; ++ind){
				//colorCode = combineColorsFourBit(r, g, b);
				colorCode = colorMethod.combineColorCode(
						rgbData[ind], 
						rgbData[ind + size], 
						rgbData[ind + doubleSize]);
				frameInfos[i].histogramValue[colorCode]++;
			}
		}
	}
	
	private void calcAllHistogramDiffs(){
		//Index of histogram diffs is NOT used
		histogramDiffs[0] = -1.0f;
		for(int i=1; i<histogramDiffs.length; ++i){
			histogramDiffs[i] = (double) calcSingleHistogramDiffSum(
					frameInfos[i-1].histogramValue, 
					frameInfos[i].histogramValue);
		}
	}
	
	private int calcSingleHistogramDiffSum(int[] lhsHistogramValue, int[] rhsHistogramValue){
		// Sigma |hBins2 - hBins1|
		int sum = 0;
		for (int i = 0; i < lhsHistogramValue.length; i++) {
			sum += Math.abs(lhsHistogramValue[i] - rhsHistogramValue[i]);
		}
		return sum;
	}
	
	private void calcMeanDeviationHistogramDiff(){
		//histogramDiffs[0] is NOT used
		meanHistogramDiff = 0;
		for(int i=1; i<histogramDiffs.length; ++i){
			meanHistogramDiff += histogramDiffs[i];
		}
		meanHistogramDiff /= (histogramDiffs.length-1);
		
		deviationHistogramDiff = 0;
		for(int i=1; i<histogramDiffs.length; ++i){
			deviationHistogramDiff += (histogramDiffs[i] - meanHistogramDiff)*
									(histogramDiffs[i] - meanHistogramDiff);
		}
		deviationHistogramDiff /= (histogramDiffs.length-1);
		deviationHistogramDiff = Math.sqrt(deviationHistogramDiff);
	}	
	
	private void detectShotBoundaries(){
		//Detect shot boundaries
		int transitionStart = -1;
		int lowValueCountThreshold = 5;
		double accumPeak = 0;
		
		for(int i=1; i<histogramDiffs.length; ++i){
			/*******************************************
			 * Twin comparison
			 */
			if(histogramDiffs[i] > cutThreshold){
				//May be a hard camera cut, test nearby differences to confirm
				//System.out.println("Candidate frame: " + i + "; Original diff: " + histogramDiffs[i]);
				if (isHardCutConfirmed(i)){
					//Confirm
					//saveFrameImage(i-1, "beforeCut");
					//saveFrameImage(i, "cut");
					cuts.add(i);
					transitionStart = -1;
					continue;
				} else {
					//Detect transition
					if (transitionStart == -1){
						transitionStart = i-1;
						accumPeak = histogramDiffs[i];
					}else{
						//Continue accumulating
						accumPeak = Math.max(accumPeak, 
								calcSingleHistogramDiffSum(
										frameInfos[transitionStart].histogramValue,
										frameInfos[i].histogramValue));
					}
				}
			}else if(histogramDiffs[i] < transitionThreshold){
				if(transitionStart != -1){
					//Check if it is end of accumulation
					boolean isEnd = true;
					for (int j=1; j<=lowValueCountThreshold; ++j){
						if (i+j < histogramDiffs.length && 
								histogramDiffs[i+j] > transitionThreshold){
							//Not an end of transition
							isEnd = false;
							break;
						}
					}
					if(!isEnd) continue;
					
					//Confirm end of transition
					if(accumPeak > cutThreshold &&
							this.calcSingleHistogramDiffSum(
									frameInfos[transitionStart].histogramValue,
									frameInfos[i].histogramValue) > cutThreshold){
						//Confirm a transition						
						//saveFrameImage(transitionStart, "start");
						starts.add(transitionStart);
						//saveFrameImage(i, "end");
						ends.add(i);
					}
					accumPeak = 0;
					transitionStart = -1;
				}
			}else{
				if(transitionStart == -1){
					//Start accumulation
					transitionStart = i-1;
					accumPeak = histogramDiffs[i];
				}else{
					//Continue accumulation
					accumPeak = Math.max(accumPeak, 
							calcSingleHistogramDiffSum(
									frameInfos[transitionStart].histogramValue,
									frameInfos[i].histogramValue));
				}
			}
		}
	}
	
	private boolean isHardCutConfirmed(int frameIndex){
		//Confirm the hard cut by test nearby differences
		int highCount = 1;
		int index=0;
		for (int i=1; i<=10; ++i){			
			index = frameIndex - i;
			//Histogram diffs start from 1
			if (index > 0 && histogramDiffs[index] > cutThreshold/2){// Tb/3 : 5000?
				++highCount;
			}
			index = frameIndex + i;
			if (index < frameInfos.length && histogramDiffs[index] > cutThreshold/2){
				++highCount;
			}
		}
		if (highCount < 2){
			//Confirm to be cut
			return true;
		}
		return false;
	}
	
	/*
	 * This doesn't work
	 */
	@Deprecated
	private int recalcHistogramDiffUsingGrids(int frameIndex){
		//Devide the image into 4 x 4 grids to compare
		byte[] rgbData;
		final int gridNumber = 16;
		FrameColorInfo[] prevFrame = new FrameColorInfo[gridNumber];
		FrameColorInfo[] currentFrame = new FrameColorInfo[gridNumber];
		for(int i=0; i<gridNumber; ++i){
			prevFrame[i] = new FrameColorInfo(frameInfos[0].histogramValue.length);
			currentFrame[i] = new FrameColorInfo(frameInfos[0].histogramValue.length);
		}		
		
		try {
			//Analyze the previous frame
			videoFile.setFramePosition(frameIndex-1);
			rgbData = videoFile.readRawData();
			calcGridHistogramValues(rgbData, prevFrame, 4, 4);
			
			//Analyze the current frame
			rgbData = videoFile.readRawData();
			calcGridHistogramValues(rgbData, currentFrame, 4, 4);
			
			double[] gridDiff = new double[gridNumber];
			for(int i=0; i<gridNumber; ++i){
				//Calculate each grid histogram difference
				gridDiff[i] = calcSingleHistogramDiffSum(prevFrame[i].histogramValue, 
						currentFrame[i].histogramValue);
			}
			Arrays.sort(gridDiff);
			int sum = 0;
			for(int i=gridNumber/4; i<gridNumber*3/4; ++i){
				sum += gridDiff[i];
			}
			return sum;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return 0;
	}
	
	private void calcGridHistogramValues(byte[] rgbData, FrameColorInfo[] frame, int columns, int rows){
		//int gridNumber = columns * rows;
		int gridWidth = videoFile.getWidth() / columns;
		int gridHeight = videoFile.getHeight() / rows;
		int colorCode = 0;
		int size = videoFile.getWidth() * videoFile.getHeight();
		int doubleSize = 2 * size;
		for(int i=0; i<frame.length; ++i){
			//Calculate histogram of each grid
			int index = i%columns * gridWidth + i/rows * gridHeight * videoFile.getWidth();
			for(int x=0; x<gridHeight; ++x){
				for(int y=0; y<gridWidth; ++y){
					colorCode = colorMethod.combineColorCode(
							rgbData[index], 
							rgbData[index + size], 
							rgbData[index + doubleSize]);
					frame[i].histogramValue[colorCode]++;
					++index;
				}
				index = index - gridWidth + videoFile.getWidth();
			}
		}
	}
	
	/*
	 * This doesn't work
	 */
	@Deprecated
	private int recalcHistogramDiffUsingMedians(int frameIndex){
		int[] temp = new int[frameInfos[frameIndex].histogramValue.length];
		for(int i=0; i<temp.length; ++i){
			temp[i] = Math.abs(
					frameInfos[frameIndex-1].histogramValue[i] - 
					frameInfos[frameIndex].histogramValue[i]);
		}
		Arrays.sort(temp);
		int end = temp.length * 3 / 4;
		int sum = 0;
		for(int i=temp.length / 4; i<end; ++i){
			sum += temp[i];
		}
		sum/=(temp.length/2);
		return sum;
	}
	
	private void saveFrameImage(long frameIndex, String frameType) {
		videoFile.setFramePosition(frameIndex);
		try {
			byte[] rgbData = videoFile.readRawData();
			int ind = 0;
			int size = videoFile.getWidth() * videoFile.getHeight();
			int doubleSize = 2 * size;
			int pixel = 0;
			byte r =0, g = 0, b = 0;
			for (int y = 0; y < videoFile.getHeight(); y++) {
				for (int x = 0; x < videoFile.getWidth(); x++) {
					r = rgbData[ind];
					g = rgbData[ind + size];
					b = rgbData[ind + doubleSize];
					pixel = 0xff000000 | ((r & 0xff) << 16) | ((g & 0xff) << 8)| (b & 0xff);
					image.setRGB(x, y, pixel);
					ind++;
				}
			}
			//Save the image to a file
			File outputfile = new File("saved" + frameIndex + "_" + frameType + ".png");
			ImageIO.write(image, "png", outputfile);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public VideoSegment[] doSegmentation(){
		int size = 1 + cuts.size() + starts.size();
		VideoSegment[] segments = new VideoSegment[size];
		int index = 1;
		int i=0;
		int j=0;
		segments[0] = new VideoSegment(0, 0);
		while(i<cuts.size() && j<starts.size()){			
			if (cuts.get(i) < starts.get(j)){
				//Add a cut
				segments[index-1].end = cuts.get(i) - 1;
				segments[index] = new VideoSegment(cuts.get(i), 0);				
				++index;
				++i;
			}else if (cuts.get(i) > starts.get(j)){
				//Add a transition
//				if (starts.get(j) - segments[index-1].start > 240){
//					//Add a start if the duration is large enough
//					segments[index-1].end = starts.get(j) - 1;
//					segments[index] = new VideoSegment(starts.get(j), 0);
//				}else{
//					//Add a transition end
//					segments[index-1].end = ends.get(j);
//					segments[index] = new VideoSegment(ends.get(j)+1, 0);
//				}
				//Simply add transition start
				segments[index-1].end = starts.get(j) - 1;
				segments[index] = new VideoSegment(starts.get(j), 0);
				++index;
				++j;
			}else{
				//Equal
				//Add this as a cut, skip the transition
				segments[index-1].end = cuts.get(i) - 1;
				segments[index] = new VideoSegment(cuts.get(i), 0);				
				++index;
				++i;
				++j;
			}
		}
		if (i!=cuts.size()){
			//Remain cuts, add them
			while (i<cuts.size()){
				segments[index-1].end = cuts.get(i)-1;
				segments[index] = new VideoSegment(cuts.get(i), 0);
				++index;
				++i;
			}
		}
		if(j!=starts.size()){
			//Remain transitions, add them
			while (j<starts.size()){
				//Add a transition
//				if (starts.get(j) - segments[index-1].start > 240){
//					//Add a start if the duration is large enough
//					segments[index-1].end = starts.get(j) - 1;
//					segments[index] = new VideoSegment(starts.get(j), 0);
//				}else{
//					//Add a transition end
//					segments[index-1].end = ends.get(j);
//					segments[index] = new VideoSegment(ends.get(j)+1, 0);
//				}
				//Simply add transition start
				segments[index-1].end = starts.get(j) - 1;
				segments[index] = new VideoSegment(starts.get(j), 0);
				++index;
				++j;
			}
		}
		//The last segment
		segments[--index].end = (int) (videoFile.getTotalFrameNumber()-1);
		VideoSegment[] retSegs = segments;
		if (index < segments.length - 1){
			retSegs = new VideoSegment[index+1];
			for(int k=0; k<retSegs.length; ++k)
				retSegs[k] = segments[k];
		}
		return retSegs;
	}
	
	public VideoSegment[] doSegmentationEx(){
		int[] tranCuts = new int[starts.size() + ends.size()];
		int tInd = 0;
		for(int k=0; k<starts.size(); ++k){
			tranCuts[tInd++] = starts.get(k);
		}
		for(int k=0; k<ends.size(); ++k){
			tranCuts[tInd++] = ends.get(k);
		}
		Arrays.sort(tranCuts);
		
		int size = 1 + cuts.size() + tranCuts.length;
		VideoSegment[] segments = new VideoSegment[size];
		int index = 1;
		int i=0;
		int j=0;
		segments[0] = new VideoSegment(0, 0);
		while(i<cuts.size() && j<tranCuts.length){			
			if (cuts.get(i) < tranCuts[j]){
				//Add a cut
				segments[index-1].end = cuts.get(i) - 1;
				segments[index] = new VideoSegment(cuts.get(i), 0);				
				++index;
				++i;
			}else if (cuts.get(i) > tranCuts[j]){
				//Add a transition
//				if (starts.get(j) - segments[index-1].start > 240){
//					//Add a start if the duration is large enough
//					segments[index-1].end = starts.get(j) - 1;
//					segments[index] = new VideoSegment(starts.get(j), 0);
//				}else{
//					//Add a transition end
//					segments[index-1].end = ends.get(j);
//					segments[index] = new VideoSegment(ends.get(j)+1, 0);
//				}
				//Simply add transition start
				segments[index-1].end = tranCuts[j] - 1;
				segments[index] = new VideoSegment(tranCuts[j], 0);
				++index;
				++j;
			}else{
				//Equal
				//Add this as a cut, skip the transition
				segments[index-1].end = cuts.get(i) - 1;
				segments[index] = new VideoSegment(cuts.get(i), 0);				
				++index;
				++i;
				++j;
			}
		}
		if (i!=cuts.size()){
			//Remain cuts, add them
			while (i<cuts.size()){
				segments[index-1].end = cuts.get(i)-1;
				segments[index] = new VideoSegment(cuts.get(i), 0);
				++index;
				++i;
			}
		}
		if(j!=tranCuts.length){
			//Remain transitions, add them
			while (j<tranCuts.length){
				//Add a transition
//				if (starts.get(j) - segments[index-1].start > 240){
//					//Add a start if the duration is large enough
//					segments[index-1].end = starts.get(j) - 1;
//					segments[index] = new VideoSegment(starts.get(j), 0);
//				}else{
//					//Add a transition end
//					segments[index-1].end = ends.get(j);
//					segments[index] = new VideoSegment(ends.get(j)+1, 0);
//				}
				//Simply add transition start
				segments[index-1].end = tranCuts[j] - 1;
				segments[index] = new VideoSegment(tranCuts[j], 0);
				++index;
				++j;
			}
		}
		//The last segment
		segments[--index].end = (int) (videoFile.getTotalFrameNumber()-1);
		VideoSegment[] retSegs = segments;
		if (index < segments.length - 1){
			retSegs = new VideoSegment[index+1];
			for(int k=0; k<retSegs.length; ++k)
				retSegs[k] = segments[k];
		}
		return retSegs;
	}
	
	public ArrayList<Integer> getStarts(){
		return starts;
	}
	
	public ArrayList<Integer> getEnds(){
		return ends;
	}
}
